昨日利用Spring Securtiy添加自定義的帳使用者驗證流程,為了方便說明沒走正常的登入流程,今天來實作登入註冊API,順便透過實作說明Spring IOC與依賴性注入
當Spring Boot應用程式啟動時會尋找特定註釋的類別,這種類別在整個應用程式被視為Bean元件,即交由IoC保管的實體物件,這種註釋可以用兩種方式分類
一個類別就是一個 Bean元件
配置很多個Bean元件
這個自動將Bean元件放入IoC容器的操作,被定義在主應用程式中添加的註釋@SpringBootApplication中,組合的註釋有@ComponentScan(自動掃描)以及@EnableAutoConfiguration(自動註冊)兩個註釋,是實現Spring Boot實現自動配置的核心配置
IoC容器能對滿足外部注入條件的地方,帶入相應資料,這部分包含了類別的欄位、方法、建構子,這個設計雖然可以很方便實現依賴反轉,但要注意有個限制,接收依賴物件的實體與給別人使用的實體,都必須由IoC容器管理,換句話說,實現依賴性注入的元件必需是Bean元件
簡單來說IoC容器解決了應用程式會使用到的元件,會依照類別內的屬性或方法參數宣告方式,決定是否由外部注入實體物件,這種讓第三者操作的設計模式,就是第12日介紹到的代理模式,細部的執行則是透過工廠模式來創建實體物件
為了方便辨識另外做一個用戶端資料的DTO物件,這裡叫他RegisterMember
// RegisterMember.java
@NoArgsConstructor
@Getter
public class RegisterMember {
private String account;
private String password;
private String name;
}
建立一個MemberService來處理登入註冊相關邏輯
@Service
public class MemberService {
/**
* @param data
*/
@Autowired
private MemberDao memberMapper;
/**
* 使用Spring Security的工具進行加密
*/
@Autowired
private PasswordEncoder pwdEncoder;
/**
* 會員註冊
*
* @param data
*
* @return void
* @throws Exception
*/
public void register(RegisterMember data) throws Exception {
Member oldMember = memberMapper.getByAccount(data.getAccount());
// Old Memebr 存在表示帳號重複使用
if (oldMember != null) {
throw new Exception("帳號已存在");
}
// 確定帳號沒問題,將密碼雜湊並建立新帳號
String password = pwdEncoder.encode(data.getPassword());
Member newMember = Member.builder()
.name(data.getName())
.account(data.getAccount())
.password(password).build();
memberMapper.create(newMember);
}
}
@Controller
@RequestMapping(path = "/api/member")
public class MemberController {
@Autowired
private MemberService memberServ;
@PostMapping("/register")
public ResponseEntity<Object> resister(
@RequestBody RegisterMember data) {
try {
memberServ.register(data);
} catch (Exception ex) {
return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(Map.of(
"status", false,
"message", "註冊失敗"));
}
return ResponseEntity.status(HttpStatus.OK).body(Map.of(
"status", true,
"message", "註冊成功"));
}
}
啟用應用程式發生錯誤
找不到PasswordEncoder的實體物件,由於是由第三方提供元件,必須在@Configure類別定義方法,實現可以添加的Bean元件,因此打開SecurityConf,定義方法回傳PasswordEncoder介面,這邊使用BCryptPasswordEncoder物件進行加解密
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
補充,先前將 /api/member 為前綴的請求,都限制實現登入驗證,這裡順便說明,可以在會員登入和註冊的請求路徑,額外添加忽略驗證配置,要注意必須放在 /api/member/** 規則之前
// 新的驗證規格
authorize
.requestMatchers("/api/member/register").permitAll()
.requestMatchers("/api/member/**").authenticated()
.anyRequest().permitAll();
重新運行 mvn spring-boor:run 啟用成功,打開Postman用先前測試的 test帳號註冊,顯示註冊失敗
在換新的帳號 newTest 註冊,資料表插入新帳號 newTest,並將密碼先雜湊在插入,到這裡註冊流程完成
登入的部分可以藉由Spring Security提供的 AuthenticationManager 進行密碼驗證,因此登入流程時建立 login路由,透過 AuthenticationManager 對要驗證的實體進行驗證
// MemberController 添加login方法
@PostMapping("/login")
public ResponseEntity<Object> login(
@RequestBody LoginMember data) {
try {
// UsernamePasswordAuthenticationToken 建構子帶兩個參數,表示當前實體需要驗證
Authentication authentication = new UsernamePasswordAuthenticationToken(
data.getAccount(), data.getPassword());
authenticationManager.authenticate(authentication);
String logingToken = jwtUtil.generateToken(data.getAccount());
return ResponseEntity.status(HttpStatus.OK).body(Map.of(
"status", true,
"token", logingToken));
} catch (Exception ex) {
System.out.println(ex.getMessage());
return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(Map.of(
"status", false,
"message", "登入失敗"));
}
}
在 SecurityConf 配置 AuthenticationProvider 和 AuthenticationManager 元件
/**
* 這裡先記重點
*
* 1. 目前只有一個 DBUserDetailsService 會從 member 資料表抓登入資料
* 2. 套用 org.springframework.security.core.userdetails.UserDetailsService
*
*/
@Autowired
public UserDetailsService dbUserService;
@Bean
public AuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setPasswordEncoder(passwordEncoder());
provider.setUserDetailsService(dbUserService);
return provider;
}
@Bean
public AuthenticationManager authManager(AuthenticationConfiguration authConf) throws Exception {
return authConf.getAuthenticationManager();
}
自己動作手登入驗證時,要從唯一的使用者帳號當作查詢條件,到DB抓取相應使用者資料,並將查詢結果進行解密,在將值跟客戶端輸入的密碼進行比對,兩個值相符才算登入成功
在控制器中使用的 authenticate 方法就是對這個步驟的封裝,差異在於要先將使用者登入資料,先轉換成身分驗證物件 UsernamePasswordAuthenticationToken(注意此處只有兩個參數),作為參數帶入 authenticate的方法中
取得帳號資料的邏輯,封裝在UserDetailsService介面要求的 loadUserByUsername 方法中,因此內部會注入 UserDetailsService 實體物件(目前只有一個從member資料表拿資料),透過帳號取得用戶資料
打開Postman對newTest帳號進行測試,注意密碼是用註冊API添加的,記得輸入自己設定的密碼
輸入錯誤密碼
輸入正確密碼成功回傳登入 token